{
 "cells": [
  {
   "cell_type": "markdown",
   "source": [
    "# Functional Usage"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "59f4e1fdfde647d4"
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "b6dca166ef24e1b8",
   "metadata": {
    "collapsed": true,
    "ExecuteTime": {
     "end_time": "2024-01-25T10:21:34.944568500Z",
     "start_time": "2024-01-25T10:21:31.280496100Z"
    }
   },
   "outputs": [],
   "source": [
    "from braandket_circuit import CNOT, FreezePass, H, M, QOperation, Sequential, allocate_qubits, compile"
   ]
  },
  {
   "cell_type": "markdown",
   "source": [
    "As we already know, a circuit can be defined using `Sequential`, `.on()` and some operations."
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "cd37d8a979b74973"
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "outputs": [],
   "source": [
    "circuit = Sequential([\n",
    "    H.on(0),\n",
    "    CNOT.on(0, 1),\n",
    "    M\n",
    "])\n"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-01-25T10:21:34.953319300Z",
     "start_time": "2024-01-25T10:21:34.946801900Z"
    }
   },
   "id": "initial_id"
  },
  {
   "cell_type": "markdown",
   "source": [
    "If you feel annoying pointing out target qubit using `.on()` and indices, do try functional usage!\n",
    "\n",
    "In functional usage, we can directly call `QOperation` instances like functions with qubits as arguments. In most cases, functional usage is more convenient and readable.\n",
    "\n",
    "Code below performs the whole circuit directly, without `Sequential` and `.on()`. As we can see, calling `M` returns a `MeasurementResult` instance.\n"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "65e3c6c1af4381c4"
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "result.value=array([0, 0], dtype=int64)\n",
      "result.prob=array(0.5)\n"
     ]
    }
   ],
   "source": [
    "q0, q1 = allocate_qubits(2)\n",
    "\n",
    "H(q0)\n",
    "CNOT(q0, q1)\n",
    "result = M(q0, q1)\n",
    "\n",
    "print(f\"{result.value=}\")\n",
    "print(f\"{result.prob=}\")"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-01-25T10:21:34.963051500Z",
     "start_time": "2024-01-25T10:21:34.950321700Z"
    }
   },
   "id": "897e3551aca61084"
  },
  {
   "cell_type": "markdown",
   "source": [
    "You may want to wrap the circuit up, distinguishing it from other parts. Then you'll find that the circuit can be defined as a function (That's why it is called \"functional\")."
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "7f6bf1742adf7e8b"
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "result.value=array([0, 0], dtype=int64)\n",
      "result.prob=array(0.5)\n"
     ]
    }
   ],
   "source": [
    "# noinspection PyRedeclaration\n",
    "def circuit(q0, q1):\n",
    "    H(q0)\n",
    "    CNOT(q0, q1)\n",
    "    return M(q0, q1)\n",
    "\n",
    "\n",
    "q0, q1 = allocate_qubits(2)\n",
    "result = circuit(q0, q1)\n",
    "\n",
    "print(f\"{result.value=}\")\n",
    "print(f\"{result.prob=}\")"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-01-25T10:21:34.965063800Z",
     "start_time": "2024-01-25T10:21:34.961738400Z"
    }
   },
   "id": "c660ef448326ca9"
  },
  {
   "cell_type": "markdown",
   "source": [
    "Furthermore, you can define your circuit as a subclass of `QOpeartion` by overriding the `__call__` method."
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "65712bc9d61e7bc8"
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "result.value=array([0, 0], dtype=int64)\n",
      "result.prob=array(0.5)\n"
     ]
    }
   ],
   "source": [
    "class MyCircuit(QOperation):\n",
    "    def __call__(self, q0, q1):\n",
    "        H(q0)\n",
    "        CNOT(q0, q1)\n",
    "        return M(q0, q1)\n",
    "\n",
    "\n",
    "q0, q1 = allocate_qubits(2)\n",
    "\n",
    "circuit = MyCircuit()\n",
    "result = circuit(q0, q1)\n",
    "\n",
    "print(f\"{result.value=}\")\n",
    "\n",
    "print(f\"{result.prob=}\")"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-01-25T10:21:35.006899700Z",
     "start_time": "2024-01-25T10:21:34.970058100Z"
    }
   },
   "id": "e69aa979ecf845af"
  },
  {
   "cell_type": "markdown",
   "source": [
    "Compared to defining a plain function, subclassing `QOperation` enables more features:\n",
    "\n",
    "* Instances `QOperation` have commonly used methods and properties, like `.on()` and `name`.\n",
    "* You can define additional methods and properties in the subclass of `QOperation`.\n",
    "* Instances of `QOperation` is recognized for compilation, optimization, visualization, execution etc."
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "261c68a3d08f4f20"
  },
  {
   "cell_type": "markdown",
   "source": [
    "Compared to constructing a `Sequential` instance, defining a circuit as a function or (subclass with `__call__`) is very different: the function is executed dynamically when you call it, while the instance of `Sequential` is static. They all have their pros and cons.\n",
    "* A dynamic circuit can be useful for debugging. For example, you can set a breakpoint in the function and inspect the intermediate quantum state.\n",
    "* A static circuit can be useful for compilation, optimization, visualization. Because the compiler (optimizer, visualizer) requires a static circuit to work on.\n",
    "\n",
    "A conversion from dynamic circuit to static circuit is supported by a special compiling process `FreezePass`. Such conversion is performed by \"tracing\". The compiler calls the operation with symbolic arguments, traces all the callings of the sub-operations and construct the static circuit according to the records. (Due to such principle, the conversion has some limitations.) An example is shown below. "
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "5bf9c37141eed590"
  },
  {
   "cell_type": "code",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Sequential([\n",
      "\tRemapped(HadamardGate(), 0),\n",
      "\tRemapped(Controlled(PauliXGate()), 0, 1),\n",
      "\tRemapped(ProjectiveMeasurement(), 0, 1),\n",
      "])\n"
     ]
    }
   ],
   "source": [
    "frozen = compile(FreezePass(), circuit)\n",
    "print(frozen)"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-01-25T10:21:35.007899500Z",
     "start_time": "2024-01-25T10:21:34.973375900Z"
    }
   },
   "id": "fd30fb643683786a",
   "execution_count": 6
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
